/*
* Copyright (C) 2006-2016 DLR, Germany
*
* All rights reserved
*
* http://www.rcenvironment.de/
*/
package de.rcenvironment.core.mail.internal;
import java.util.concurrent.Future;
import javax.mail.AuthenticationFailedException;
import javax.mail.MessagingException;
import jodd.mail.Email;
import jodd.mail.MailException;
import jodd.mail.SendMailSession;
import jodd.mail.SmtpSslServer;
import org.apache.commons.lang3.concurrent.ConcurrentUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import de.rcenvironment.core.configuration.ConfigurationException;
import de.rcenvironment.core.configuration.ConfigurationSegment;
import de.rcenvironment.core.configuration.ConfigurationService;
import de.rcenvironment.core.mail.Mail;
import de.rcenvironment.core.mail.MailDispatchResult;
import de.rcenvironment.core.mail.MailDispatchResultListener;
import de.rcenvironment.core.mail.MailService;
import de.rcenvironment.core.mail.SMTPServerConfiguration;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.toolkit.modules.concurrency.api.TaskDescription;
/**
* This class is an implementation of the {@link MailService}.
*
* @author Tobias Rodehutskors
*/
public final class MailServiceImpl implements MailService {
private Log log = LogFactory.getLog(MailServiceImpl.class);
private ConfigurationService configurationService;
private SMTPServerConfiguration mailConfiguration;
// False, if there is no mail configuration or the configuration is invalid; True, otherwise.
private boolean configured = false;
private boolean trustAllCertificates = false;
/**
* OSGi-DS life-cycle method.
*/
public void activate() {
ConfigurationSegment configurationSegment =
configurationService.getConfigurationSegment(SMTPServerConfiguration.CONFIGURATION_PATH);
if (configurationSegment == null || !configurationSegment.isPresentInCurrentConfiguration()) {
configured = false;
log.debug("MailService not started as there is no mail configuration at all.");
} else {
mailConfiguration =
new SMTPServerConfiguration(configurationSegment, SMTPServerConfiguration.getMailFilterInformation(configurationService));
try {
mailConfiguration.isValid(); // throws an exception if the configuration is invalid
configured = true;
log.debug("MailService is configured.");
} catch (ConfigurationException e) {
configured = false;
log.error(StringUtils.format("MailService is not successfully configured since the configuration is invalid: %s ",
e.getMessage()));
}
}
}
// public void deactivate() {
// TODO cancel all futures of the MailSendingHandler if the service is deactivated
// TODO and write a test for that
// }
protected void bindConfigurationService(ConfigurationService newConfigurationService) {
this.configurationService = newConfigurationService;
}
@Override
public Future<?> sendMail(Mail mail, MailDispatchResultListener listener) {
if (!configured) {
listener.receiveResult(MailDispatchResult.FAILURE_MAIL_SERVICE_NOT_CONFIGURED, null);
return ConcurrentUtils.constantFuture(null);
}
MailSendingHandler mailSendingTask = new MailSendingHandler(mail, listener);
return ConcurrencyUtils.getAsyncTaskService().submit(mailSendingTask);
}
/**
* This runnable is used to send mails asynchronously.
*/
private class MailSendingHandler implements Runnable {
private static final String CAN_T_SEND_COMMAND_TO_SMTP_HOST = "Can't send command to SMTP host";
private static final String INTERRUPTED_WHILE_WAITING = "Mail delivery was interrupted while waiting for the next attempt.";
private static final int SEC_TO_MILLIS = 1000;
private static final String SEND_FAILED = "Sending mail failed.";
private static final int AUTO_RETRY_INITIAL_DELAY_MILLIS = 5000;
private static final double AUTO_RETRY_DELAY_MULTIPLIER = 1.5;
private static final int AUTO_RETRY_MAX_DELAY_MILLIS = 300000;
private Email mail;
private MailDispatchResultListener listener;
private int consecutiveConnectionFailures;
MailSendingHandler(Mail mail, MailDispatchResultListener listener) {
this.mail = mail.getMail();
this.listener = listener;
consecutiveConnectionFailures = 0;
}
@Override
@TaskDescription("Sending mail")
public void run() {
boolean success = false;
// flag to indicate whether we should retry the mail delivery
boolean permFailed = false;
String permFailedMessage = null;
while (!success && !permFailed) {
// try to send the mail
try {
smtpServerAction(mail);
success = true;
} catch (MailException e) {
log.error(SEND_FAILED, e);
success = false;
consecutiveConnectionFailures++;
permFailed = isMailExceptionCausedByPermError(e);
if (permFailed) {
permFailedMessage = e.getCause().getMessage();
}
}
// notify the listener about the result of the mail delivery attempt
if (listener != null) {
if (success) {
listener.receiveResult(MailDispatchResult.SUCCESS, null);
} else {
if (permFailed) {
log.debug(StringUtils.format("Sending %s to MailDispatchResultListener.",
MailDispatchResult.FAILURE.toString()));
listener.receiveResult(MailDispatchResult.FAILURE, permFailedMessage);
} else {
listener.receiveResult(MailDispatchResult.FAILURE_RETRY, null);
}
}
}
// sleep some time if we should retry
if (!success && !permFailed) {
try {
long autoRetryDelay = calculateNextAutoRetryDelay();
log.debug(StringUtils.format("Retrying mail delivery in %d seconds.", (autoRetryDelay / SEC_TO_MILLIS)));
Thread.sleep(autoRetryDelay);
log.debug("Thread.currentThread().isInterrupted(): " + Thread.currentThread().isInterrupted());
} catch (InterruptedException e) {
log.debug(INTERRUPTED_WHILE_WAITING);
listener.receiveResult(MailDispatchResult.FAILURE, INTERRUPTED_WHILE_WAITING);
return;
}
}
}
}
private long calculateNextAutoRetryDelay() {
long targetDelay =
Math.round(AUTO_RETRY_INITIAL_DELAY_MILLIS * Math.pow(AUTO_RETRY_DELAY_MULTIPLIER, consecutiveConnectionFailures - 1));
// apply upper limit, if set
targetDelay = Math.min(targetDelay, AUTO_RETRY_MAX_DELAY_MILLIS);
return targetDelay;
}
/**
* We need to decide if it worth to attempt a new delivery or if the failure indicates a permanent error like a configuration
* mistake. For this purpose this method evaluates the cause of a MailExeption.
*
* @param e MailExeption whose cause should be analyzed.
* @return True in case of an configuration error; False, otherwise.
*/
private boolean isMailExceptionCausedByPermError(MailException e) {
Throwable cause = e.getCause();
if (cause != null) {
// if we receive a MessagingException we not not try to redeliver the mail...
// ... except if the detail message is the specified one
// this check is used instead of instanceof to be sure to only match MessagingExceptions and not its subclasses
if ((cause.getClass().equals(MessagingException.class)
&& !(cause.getMessage().equals(CAN_T_SEND_COMMAND_TO_SMTP_HOST)))
// if we receive an AuthenticationFailedException we do not try to redeliver the mail
// AuthenticationFailedException extends MessagingException
|| cause instanceof AuthenticationFailedException) {
// configuration error
return true;
}
}
// no configuration error
return false;
}
}
/**
* @return True, if a connection to the mail server can be established.
*/
public boolean canConnectToServer() {
if (!configured) {
log.error("SMTP server is not configured.");
return false;
}
try {
smtpServerAction(null);
} catch (MailException e) {
log.error("Unable to contact the configured SMTP server.", e);
return false;
}
return true;
}
/**
* Sends a mail to the configured mail server.
*
* @param mail If null, this method will still connect to the server, but not send a mail.
* @throws MailException Can have one of the following causes:
*
* <table border="1">
* <tbody>
* <tr>
* <th>Cause exception</th>
* <th> Nested exception</th>
* <th>Detail message</th>
* <th>Reason</th>
* </tr>
* <tr>
* <td>com.sun.mail.util.MailConnectException</td>
* <td>java.net.UnknownHostException</td>
* <td>TODO</td>
* <td>Host might be temporary offline OR host does not exist at all.</td>
* </tr>
* <tr>
* <td>com.sun.mail.util.MailConnectException</td>
* <td>java.net.ConnectException</td>
* <td>TODO</td>
* <td>High workload OR wrong port is configured</td>
* </tr>
* <tr>
* <td>javax.mail.AuthenticationFailedException</td>
* <td> </td>
* <td>TODO</td>
* <td>User name and/or password are not correct</td>
* </tr>
* <tr>
* <td>javax.mail.MessagingException</td>
* <td> </td>
* <td>TODO</td>
* <td>Connecting with explicit encryption to a port expecting implicit encryption</td>
* </tr>
* <tr>
* <td>javax.mail.MessagingException</td>
* <td>javax.net.ssl.SSLException</td>
* <td>TODO</td>
* <td>Connecting with implicit encryption to a port expecting explicit encryption</td>
* </tr>
* <tr>
* <td>javax.mail.MessagingException</td>
* <td>next: SocketException</td>
* <td>Can't send command to SMTP host</td>
* <td>The SMTP server closed the connection, e.g. because of rate limits. Retry later.</td>
* </tr>
* </tbody>
* </table>
*/
private void smtpServerAction(Email mail) throws MailException {
SendMailSession session = null;
try {
SmtpSslServer server = new SmtpSslServer(mailConfiguration.getHost(), mailConfiguration.getPort())
.authenticateWith(mailConfiguration.getUsername(), mailConfiguration.getPassword());
// if the encryption is not set to 'explicit', 'implicit' mode is automatically assumed which requires no special flags
if (SMTPServerConfiguration.EXPLICIT_ENCRYPTION.equals(mailConfiguration.getEncryption())) {
// if STARTTLS is not supported by the server, session opening should fail
server.startTlsRequired(true);
// needed in conjunction with startTlsRequired
server.plaintextOverTLS(true);
}
if (trustAllCertificates) {
server.property("mail.smtp.ssl.trust", "*");
}
// server.debug(true);
session = server.createSession();
session.open();
if (mail != null) {
mail.from(mailConfiguration.getSender());
session.sendMail(mail);
}
} finally {
if (session != null) {
session.close();
}
}
};
@Override
public boolean isConfigured() {
return configured;
}
/**
* NOT SECURE! This method is only intended to be used in unit test. Do not call this function in production code!
*
* Disables SSL certificate checks for all connections.
*/
void trustAllCertificates() {
trustAllCertificates = true;
}
}